feat: add Contributor Activity Matrix with GitHub API integration#33
feat: add Contributor Activity Matrix with GitHub API integration#33Muneerali199 wants to merge 1 commit intoAOSSIE-Org:mainfrom
Conversation
WalkthroughThis pull request introduces a "Contributor Activity Matrix" dashboard feature for the OrgExplorer application. It adds React Router-based multi-page navigation, a comprehensive contributor activity view with filtering and export capabilities, GitHub API integration with caching and rate limiting, and associated UI components for displaying contributor statistics and activity matrices. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant App as App (React Router)
participant Feature as ContributorActivityFeature
participant GitHubTokenInput
participant LocalStorage
participant Client as GitHub API Client
participant API as GitHub API
participant UseCases as Use Cases
participant Matrix as ContributorMatrix
User->>App: Navigate to /contributors/:org
App->>Feature: Render with org param & fromDate
Feature->>GitHubTokenInput: Initialize token input
GitHubTokenInput->>LocalStorage: Read stored token
LocalStorage-->>GitHubTokenInput: Return token (if exists)
GitHubTokenInput-->>Feature: Invoke onTokenChange callback
Feature->>Feature: Set loading state
Feature->>UseCases: FetchContributorActivityUseCase.execute(org, fromDate)
UseCases->>Client: fetchAllOrgRepositories(org)
Client->>API: GET /repos?org=X
API-->>Client: Return repos with pagination
Client-->>UseCases: Return full repo list
par Parallel Requests
UseCases->>Client: searchAllPRs(org, fromDate)
UseCases->>Client: searchAllIssues(org, fromDate)
end
Client->>API: GET /search?q=org:X is:pr created:>date
Client->>API: GET /search?q=org:X is:issue created:>date
API-->>Client: Return PRs and Issues
Client-->>UseCases: Return aggregated data
UseCases->>UseCases: transformToContributorMatrix()
UseCases-->>Feature: Return ContributorMatrixData
Feature->>UseCases: GetContributorStatsUseCase.execute(data)
UseCases-->>Feature: Return stats
Feature->>Feature: Apply filters & search
Feature->>Feature: Set data state
Feature->>Matrix: Render with data & repos
Matrix-->>User: Display activity matrix
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 24
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/App.tsx`:
- Around line 11-18: The JSX includes hardcoded user-visible strings (e.g., the
<h1> "OrgExplorer" and Link labels inside the div.nav-links) which must be
externalized; update the App component to replace these literal strings with
i18n lookup keys (e.g., use a translation function like t('nav.title') and
t('nav.home'), t('nav.contributors.aossie') etc.), import and use the project's
i18n helper (or react-i18next useTranslation) at the top of the component, and
add corresponding entries to the resource file(s); ensure the same refactor is
applied for the other hardcoded strings referenced (lines ~38-53) so all
user-visible text comes from the localization resources.
- Around line 60-64: Validate and sanitize the URL-derived values before passing
them into ContributorActivityFeature: parse and decode the org from
window.location.pathname using decodeURIComponent and a safer extraction (e.g.,
take the last non-empty segment), trim and validate it against a whitelist regex
(alphanumeric, hyphen, underscore) and fallback to 'AOSSIE-Org' if it fails; for
fromDate (params.get('from')), validate it as an ISO date (YYYY-MM-DD) or use
Date.parse to ensure it's a real date, normalize it to the expected format (or
set to undefined) and only pass the validated values into
ContributorActivityFeature (references: params, org, fromDate,
ContributorActivityFeature).
In `@src/features/contributor-activity/components/ContributorFilters.tsx`:
- Around line 27-32: The search input in ContributorFilters (the input using
value={searchQuery} and onChange={e => onSearchChange(e.target.value)}) needs an
accessible name—add a visible <label> or an aria-label/aria-labelledby that
describes it (e.g., "Search contributors") so screen readers can identify it;
also locate the buttons in the same component (the ones around the 74-80 area)
and ensure each has an explicit type attribute (e.g., type="button" or
type="submit" as appropriate) to prevent unintended form submissions.
In `@src/features/contributor-activity/components/ContributorMatrix.tsx`:
- Around line 31-48: The JSX in ContributorMatrix contains hardcoded
user-visible strings (“Contributor”, “Total”, the no-activity message, and any
"Less"/"More" labels) which must be externalized for i18n; update the
ContributorMatrix component to import and use the project’s localization utility
(e.g., a t() or useTranslation hook) and replace the literal strings in the
render (including the table header labels, the no-results paragraph, and any
pagination/buttons that render "Less"/"More") with localized keys (e.g.,
t('contributorMatrix.contributor'), t('contributorMatrix.total'),
t('contributorMatrix.noActivity'), etc.), add those keys to the resource files,
and ensure title/tooltips like repo header titles also use the localized strings
where appropriate so all user-facing text comes from the resource bundle instead
of hardcoded literals.
- Around line 12-13: Replace the hard-coded "15" repository limit with a named
constant to avoid duplication: define a constant (e.g. const REPO_DISPLAY_LIMIT
= 15) near the top of ContributorMatrix.tsx and replace uses of the literal in
the expressions that create displayRepos (allRepositories.slice(0, 15)) and
hasMoreRepos (allRepositories.length > 15) as well as any other occurrences (the
logic around line 81) to reference REPO_DISPLAY_LIMIT so the limit is maintained
in one place.
- Around line 15-18: getActivityCount currently does a linear search through
data.contributors for each cell; switch it to use the O(1) lookup provided by
data.activityByContributor (from ContributorMatrixData). In
ContributorMatrix.tsx replace the find-based logic in getActivityCount with a
lookup like: get the contributorMap = data.activityByContributor.get(login) and
then return contributorMap?.get(repo) || 0, ensuring you handle missing map or
missing repo by returning 0. Also keep the function signature and callers
unchanged so rendering logic still receives a number.
- Around line 60-66: The anchor HREF should not branch on
contributor.login.includes('http'); update the ContributorMatrix component to
always generate profile links using `https://github.com/${contributor.login}`
and remove the defensive includes('http') check; instead validate/normalize
logins when data is fetched or transformed (e.g., in your contributor data
mapper or a normalizeContributors / mapContributors function) to strip/handle
any URLs or malformed values and ensure contributor.login is a plain GitHub
username before it reaches ContributorMatrix.
In `@src/features/contributor-activity/components/ContributorStats.tsx`:
- Around line 21-26: The component ContributorStats currently hardcodes
user-visible strings (e.g., "Total Contributors", "Avg. Contributions", "Top
Contributors", "Most Active Repos") inside the JSX; replace those literals with
references to i18n resource keys and use the existing localization helper (e.g.,
call the i18n/t function or useTranslation hook used in the project) inside
ContributorStats so the headings and any labels render via resource strings
(e.g., t('contributorStats.totalContributors'),
t('contributorStats.avgContributions'), etc.), add the corresponding keys and
translations to the locale resource file(s), and ensure stats.totalContributors
and stats.averageContributions remain unchanged while labels come from the i18n
keys.
In `@src/features/contributor-activity/components/ErrorState.tsx`:
- Around line 6-17: The ErrorState component renders a hardcoded "Retry" label;
replace this with an externalized i18n string: add a resource key (e.g.,
"retry") to your locale files and consume it in ErrorState (use your app's
localization API such as useTranslation/useIntl or accept a translated prop)
instead of the literal "Retry" in the button; update the locale resource files
to include the new key and ensure a sensible fallback is used if the translation
is missing.
- Around line 11-13: The Retry button in the ErrorState component defaults to
type="submit" which can trigger form submissions; update the button element that
uses onRetry and className "btn-retry" to include an explicit type="button"
attribute so clicking it only invokes the onRetry handler and never submits a
surrounding form.
In `@src/features/contributor-activity/components/GitHubTokenInput.tsx`:
- Around line 39-46: The label in the GitHubTokenInput component is not tied to
the input and the toggle/clear action buttons lack explicit button semantics;
add a unique id for the input (e.g., tokenInputId) and set the label's htmlFor
to that id so the label is associated with the input (affecting the input
controlled by value={token}, onChange={setToken} and type driven by showToken),
and update the action elements (the show/hide toggle and any clear/save buttons
referenced around the token input, lines with showToken and token state
handling) to use <button type="button"> semantics rather than implicit buttons
or divs to ensure correct accessibility and prevent accidental form submissions.
- Around line 14-19: The useEffect reads TOKEN_KEY from localStorage and calls
onTokenChange(savedToken) but does not initialize the component's local input
state, leaving the input empty and hiding the Clear action; update the effect to
set the component's token state as well (e.g., call setToken(savedToken) or
initialize token state with localStorage.getItem(TOKEN_KEY)) so the input
displays the persisted value and the Clear action is visible; ensure this change
touches the same effect using TOKEN_KEY and uses the existing state setter
(setToken) alongside onTokenChange to avoid redundant reloads.
- Around line 15-33: The component currently persists the PAT using localStorage
(TOKEN_KEY) in useEffect, handleSave and handleClear which exposes sensitive
tokens; remove all localStorage.getItem/setItem/removeItem calls and keep the
token only in React state (token) and via onTokenChange, or alternatively send
the token to a secure backend endpoint (or use sessionStorage if ephemeral
storage is required) instead of localStorage; update handleSave to avoid writing
to localStorage (setSaved behavior can remain but should be driven by
state/response from backend if used), and update handleClear to only clear state
and notify onTokenChange('') without removing localStorage. Ensure TOKEN_KEY
usage is eliminated and onTokenChange remains the mechanism to propagate the
token to the rest of the app or backend.
In `@src/features/contributor-activity/components/LoadingState.tsx`:
- Around line 1-8: The LoadingState component currently hardcodes the message
and lacks ARIA attributes; update the LoadingState React.FC to retrieve the
display string from the i18n/messages system (replace the literal "Loading
contributor activity..." with the localized message key) and add proper
accessibility attributes to the container and spinner (e.g., role="status" or
aria-live="polite" on the text container and aria-hidden/aria-busy as
appropriate on the spinner) so screen readers announce the state; update
references in the component (LoadingState, the spinner div and the paragraph) to
use the localized string and ARIA attributes.
In `@src/features/contributor-activity/ContributorActivityFeature.tsx`:
- Around line 171-173: The Clear Cache button currently lacks an explicit type
which can cause it to act as a submit button in forms; update the JSX for the
button element (the one using onClick={handleClearCache} and
className="btn-clear-cache") to include type="button" so it won't trigger form
submission inadvertently and continues to call the handleClearCache handler.
In `@src/features/contributor-activity/useCases.ts`:
- Around line 81-84: The JSON branch of execute in ContributorMatrixData
currently calls JSON.stringify(data) which loses Map contents because
contributors[].repositories and activityByContributor are Maps; convert those
Maps to plain objects/arrays before stringifying: iterate contributors (in the
execute function) and replace each contributor.repositories Map with an object
(or array of [key,value] pairs), and similarly transform activityByContributor
(Map<string, Map<string, number>>) into a nested plain object or serializable
structure, then call JSON.stringify on the transformed object so all map data is
preserved.
In `@src/lib/github/api.ts`:
- Around line 55-57: Guard against empty repository names before inserting into
collections: when computing repoName (from pr.repository_url?.split('/').pop()),
check that repoName is a non-empty string before calling repoSet.add(repoName)
and before updating contributor repositories and the activity matrix (the
analogous updates around lines 115-117). Wrap those add/update calls in a
conditional like if (repoName) { ... } so blank strings are never used as keys.
- Around line 10-13: searchAllIssues currently filters only open issues which
undercounts contributor activity; update the searchAllIssues query to remove the
"is:open" qualifier (or explicitly include both open and closed) so all issues
are returned, then re-run the existing aggregation that consumes
searchAllIssues; also fix repoName handling where repository_url may be missing
by avoiding defaulting to an empty string—change the logic that derives repoName
(the variable computed from repository_url in api functions that aggregate
contributors after calling searchAllPRs/searchAllIssues/fetchAllOrgRepositories)
to either skip entries with no repository_url or use a non-empty sentinel (e.g.,
"unknown" or null) and ensure contributor maps keyed by repoName do not get
blank keys (filter out or normalize empty repoName before inserting into
contributor maps).
In `@src/lib/github/client.ts`:
- Around line 63-66: The code reads a persisted GitHub token from localStorage
(the token variable) and sets headers['Authorization'], which exposes
credentials; remove the localStorage.getItem('github_token') usage and stop
persisting PATs client-side. Change the consumer of this module to pass an
ephemeral token into the function that builds headers (or wire the function to
fetch auth from a secure server-side proxy), and update the code path that sets
headers['Authorization'] (the assignment to headers['Authorization'] = `token
${token}`) to only run when a token is provided via the new parameter or secure
backend call; also remove any code that writes tokens into localStorage.
- Line 285: The searchAllIssues implementation currently forces only open issues
by building the query string with "is:open"; update the query construction in
searchAllIssues to remove or make the "is:open" filter conditional so it can
include closed issues when callers request "all" (or default to no state filter
for an all-issues endpoint). Locate the query string assembly where `const query
= \`org:${org} is:issue is:open${sinceDate ? \` created:>${sinceDate}\` :
''}\`;` is defined and adjust it to either omit `is:open` or accept a parameter
(e.g., issueState) to append `is:open` or `is:closed` only when needed, ensuring
callers can request all issues. Ensure tests or call sites of searchAllIssues
are updated to pass the desired state when appropriate.
- Around line 133-140: The current loop calls getRateLimit() before each request
which doubles API calls; instead remove the preflight getRateLimit() invocation
in the retry loop and extract rate-limit info from the actual request response
headers (e.g., read response.headers.get('x-ratelimit-remaining') and
'x-ratelimit-reset') to decide backoff and delay; update the retry logic inside
the for-loop that uses maxRetries/attempt and delay() so it inspects those
headers after a response (or on error when available) and computes waitTime =
(reset - Date.now()/1000)*1000, logging and awaiting delay(waitTime) when
remaining < 1 and waitTime > 0.
- Around line 3-7: Remove the duplicate RateLimitInfo interface from
src/lib/github/client.ts and instead import the existing type from the shared
declaration (src/lib/github/types.ts); specifically delete the local
RateLimitInfo declaration and add an import for RateLimitInfo at the top of
client.ts, then ensure any references in functions like the client methods still
use the imported RateLimitInfo type.
In `@src/lib/github/types.ts`:
- Around line 56-60: The RateLimitInfo interface is duplicated; consolidate to a
single source by keeping one declaration and importing it where needed: either
export RateLimitInfo from the types module and remove the duplicate in the
client module (then import RateLimitInfo in client.ts), or delete RateLimitInfo
from types.ts and import the single definition from client.ts; update any
imports/usages to reference the retained symbol RateLimitInfo and remove the
redundant interface declaration.
- Around line 34-42: The JSON export currently serializes ContributorMatrixData
containing Map fields (repositories and activityByContributor) which become
empty objects; in ExportContributorDataUseCase.execute convert any Map<string,
number> fields (e.g., ContributorSummary.repositories and top-level
activityByContributor in ContributorMatrixData) into plain objects before
calling JSON.stringify — for example, replace Maps with Object.fromEntries(map)
or build a plain object via Array.from(map.entries()) for each map, or supply a
JSON.stringify replacer that transforms Map instances to plain objects; ensure
you perform this transformation for ContributorSummary objects inside the matrix
as well as the top-level maps so the exported JSON contains the actual key/value
pairs.
ℹ️ Review info
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (17)
.env.examplepackage.jsonsrc/App.csssrc/App.tsxsrc/features/contributor-activity/ContributorActivityFeature.tsxsrc/features/contributor-activity/components/ContributorFilters.tsxsrc/features/contributor-activity/components/ContributorMatrix.tsxsrc/features/contributor-activity/components/ContributorStats.tsxsrc/features/contributor-activity/components/ErrorState.tsxsrc/features/contributor-activity/components/GitHubTokenInput.tsxsrc/features/contributor-activity/components/LoadingState.tsxsrc/features/contributor-activity/index.tssrc/features/contributor-activity/useCases.tssrc/index.csssrc/lib/github/api.tssrc/lib/github/client.tssrc/lib/github/types.ts
| <h1>OrgExplorer</h1> | ||
| </div> | ||
| <div className="nav-links"> | ||
| <Link to="/">Home</Link> | ||
| <Link to="/contributors/AOSSIE-Org">AOSSIE Contributors</Link> | ||
| <Link to="/contributors/StabilityNexus">StabilityNexus Contributors</Link> | ||
| <Link to="/contributors/DjedAlliance">DjedAlliance Contributors</Link> | ||
| </div> |
There was a problem hiding this comment.
Externalize user-visible strings instead of hardcoding them in components.
The new navigation/homepage text is directly embedded in JSX, which blocks localization scalability.
As per coding guidelines, "User-visible strings should be externalized to resource files (i18n)."
Also applies to: 38-53
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/App.tsx` around lines 11 - 18, The JSX includes hardcoded user-visible
strings (e.g., the <h1> "OrgExplorer" and Link labels inside the div.nav-links)
which must be externalized; update the App component to replace these literal
strings with i18n lookup keys (e.g., use a translation function like
t('nav.title') and t('nav.home'), t('nav.contributors.aossie') etc.), import and
use the project's i18n helper (or react-i18next useTranslation) at the top of
the component, and add corresponding entries to the resource file(s); ensure the
same refactor is applied for the other hardcoded strings referenced (lines
~38-53) so all user-visible text comes from the localization resources.
| const params = new URLSearchParams(window.location.search); | ||
| const org = window.location.pathname.split('/').pop() || 'AOSSIE-Org'; | ||
| const fromDate = params.get('from') || undefined; | ||
|
|
||
| return <ContributorActivityFeature organization={org} defaultFromDate={fromDate} />; |
There was a problem hiding this comment.
Validate URL-derived org and from before passing them downstream.
from is forwarded without format validation, and org parsing is brittle. This can produce malformed GitHub search qualifiers and inconsistent results.
🔧 Proposed fix
const ContributorActivityFeatureWrapper: React.FC = () => {
const params = new URLSearchParams(window.location.search);
- const org = window.location.pathname.split('/').pop() || 'AOSSIE-Org';
- const fromDate = params.get('from') || undefined;
+ const orgCandidate = window.location.pathname.split('/').filter(Boolean).pop() ?? 'AOSSIE-Org';
+ const org = /^[A-Za-z0-9-]+$/.test(orgCandidate) ? orgCandidate : 'AOSSIE-Org';
+ const fromParam = params.get('from');
+ const fromDate = fromParam && /^\d{4}-\d{2}-\d{2}$/.test(fromParam) ? fromParam : undefined;
return <ContributorActivityFeature organization={org} defaultFromDate={fromDate} />;
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/App.tsx` around lines 60 - 64, Validate and sanitize the URL-derived
values before passing them into ContributorActivityFeature: parse and decode the
org from window.location.pathname using decodeURIComponent and a safer
extraction (e.g., take the last non-empty segment), trim and validate it against
a whitelist regex (alphanumeric, hyphen, underscore) and fallback to
'AOSSIE-Org' if it fails; for fromDate (params.get('from')), validate it as an
ISO date (YYYY-MM-DD) or use Date.parse to ensure it's a real date, normalize it
to the expected format (or set to undefined) and only pass the validated values
into ContributorActivityFeature (references: params, org, fromDate,
ContributorActivityFeature).
| <input | ||
| type="text" | ||
| placeholder="Search contributors..." | ||
| value={searchQuery} | ||
| onChange={(e) => onSearchChange(e.target.value)} | ||
| /> |
There was a problem hiding this comment.
Add accessible naming for search input and explicit button types.
🔧 Proposed fix
<input
type="text"
+ aria-label="Search contributors"
placeholder="Search contributors..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
/>
@@
- <button onClick={onRefresh} className="btn-refresh">
+ <button type="button" onClick={onRefresh} className="btn-refresh">
Refresh
</button>
- <button onClick={() => onExport('csv')} className="btn-export">
+ <button type="button" onClick={() => onExport('csv')} className="btn-export">
Export CSV
</button>
- <button onClick={() => onExport('json')} className="btn-export">
+ <button type="button" onClick={() => onExport('json')} className="btn-export">
Export JSON
</button>Also applies to: 74-80
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/contributor-activity/components/ContributorFilters.tsx` around
lines 27 - 32, The search input in ContributorFilters (the input using
value={searchQuery} and onChange={e => onSearchChange(e.target.value)}) needs an
accessible name—add a visible <label> or an aria-label/aria-labelledby that
describes it (e.g., "Search contributors") so screen readers can identify it;
also locate the buttons in the same component (the ones around the 74-80 area)
and ensure each has an explicit type attribute (e.g., type="button" or
type="submit" as appropriate) to prevent unintended form submissions.
| const displayRepos = allRepositories.slice(0, 15); | ||
| const hasMoreRepos = allRepositories.length > 15; |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Extract magic number to a named constant.
The limit of 15 repositories appears twice (lines 12, 81). Extract to a constant for maintainability.
♻️ Suggested refactor
+const MAX_DISPLAY_REPOS = 15;
+
export const ContributorMatrix: React.FC<ContributorMatrixProps> = ({
data,
allRepositories,
}) => {
- const displayRepos = allRepositories.slice(0, 15);
- const hasMoreRepos = allRepositories.length > 15;
+ const displayRepos = allRepositories.slice(0, MAX_DISPLAY_REPOS);
+ const hasMoreRepos = allRepositories.length > MAX_DISPLAY_REPOS;Also update line 81:
- {hasMoreRepos && <td className="more-cell">+{allRepositories.length - 15}</td>}
+ {hasMoreRepos && <td className="more-cell">+{allRepositories.length - MAX_DISPLAY_REPOS}</td>}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/contributor-activity/components/ContributorMatrix.tsx` around
lines 12 - 13, Replace the hard-coded "15" repository limit with a named
constant to avoid duplication: define a constant (e.g. const REPO_DISPLAY_LIMIT
= 15) near the top of ContributorMatrix.tsx and replace uses of the literal in
the expressions that create displayRepos (allRepositories.slice(0, 15)) and
hasMoreRepos (allRepositories.length > 15) as well as any other occurrences (the
logic around line 81) to reference REPO_DISPLAY_LIMIT so the limit is maintained
in one place.
| const getActivityCount = (login: string, repo: string): number => { | ||
| const contributor = data.contributors.find(c => c.login === login); | ||
| return contributor?.repositories.get(repo) || 0; | ||
| }; |
There was a problem hiding this comment.
Inefficient O(n) lookup for each cell; use activityByContributor Map instead.
getActivityCount performs a linear search through data.contributors for every cell rendered. With C contributors and R repositories, this results in O(C × R × C) complexity.
The data.activityByContributor Map (defined in ContributorMatrixData) appears designed for O(1) lookups but isn't being used.
🔧 Proposed fix using the existing Map
const getActivityCount = (login: string, repo: string): number => {
- const contributor = data.contributors.find(c => c.login === login);
- return contributor?.repositories.get(repo) || 0;
+ return data.activityByContributor.get(login)?.get(repo) || 0;
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/contributor-activity/components/ContributorMatrix.tsx` around
lines 15 - 18, getActivityCount currently does a linear search through
data.contributors for each cell; switch it to use the O(1) lookup provided by
data.activityByContributor (from ContributorMatrixData). In
ContributorMatrix.tsx replace the find-based logic in getActivityCount with a
lookup like: get the contributorMap = data.activityByContributor.get(login) and
then return contributorMap?.get(repo) || 0, ensuring you handle missing map or
missing repo by returning 0. Also keep the function signature and callers
unchanged so rendering logic still receives a number.
| const token = localStorage.getItem('github_token'); | ||
| if (token) { | ||
| headers['Authorization'] = `token ${token}`; | ||
| } |
There was a problem hiding this comment.
Do not persist GitHub tokens in localStorage.
Storing PATs in localStorage increases exposure to XSS/token theft. Move token handling to a server-side proxy (preferred) or at minimum avoid persistent client-side storage for sensitive credentials.
As per coding guidelines, Security requires "No exposed API keys or sensitive data".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/github/client.ts` around lines 63 - 66, The code reads a persisted
GitHub token from localStorage (the token variable) and sets
headers['Authorization'], which exposes credentials; remove the
localStorage.getItem('github_token') usage and stop persisting PATs client-side.
Change the consumer of this module to pass an ephemeral token into the function
that builds headers (or wire the function to fetch auth from a secure
server-side proxy), and update the code path that sets headers['Authorization']
(the assignment to headers['Authorization'] = `token ${token}`) to only run when
a token is provided via the new parameter or secure backend call; also remove
any code that writes tokens into localStorage.
| for (let attempt = 0; attempt < maxRetries; attempt++) { | ||
| const rateLimit = await getRateLimit(); | ||
|
|
||
| if (rateLimit.remaining < 1) { | ||
| const waitTime = (rateLimit.reset - Date.now() / 1000) * 1000; | ||
| if (waitTime > 0 && waitTime < 60000) { | ||
| console.log(`Rate limit hit. Waiting ${Math.round(waitTime / 1000)}s...`); | ||
| await delay(waitTime); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's look at the structure and the specific file
find . -name "client.ts" -path "*/github/*" | head -20Repository: AOSSIE-Org/OrgExplorer
Length of output: 92
🏁 Script executed:
# Read the file to understand the context around lines 133-140
wc -l src/lib/github/client.tsRepository: AOSSIE-Org/OrgExplorer
Length of output: 94
🏁 Script executed:
# Get the relevant section
sed -n '120,160p' src/lib/github/client.tsRepository: AOSSIE-Org/OrgExplorer
Length of output: 1498
🏁 Script executed:
# Find the getRateLimit function definition
rg -n "getRateLimit" src/lib/github/client.ts -A 10Repository: AOSSIE-Org/OrgExplorer
Length of output: 831
🏁 Script executed:
# Search for getRateLimit definition anywhere in the codebase
rg -n "function getRateLimit|const getRateLimit|getRateLimit\s*=" --type ts --type tsxRepository: AOSSIE-Org/OrgExplorer
Length of output: 93
🏁 Script executed:
# Check more of the fetchWithRetry function
sed -n '130,170p' src/lib/github/client.tsRepository: AOSSIE-Org/OrgExplorer
Length of output: 1524
🏁 Script executed:
# Search for usage of x-ratelimit headers
rg -n "x-ratelimit|ratelimit" src/lib/github/client.tsRepository: AOSSIE-Org/OrgExplorer
Length of output: 48
🏁 Script executed:
# Check the full getRateLimit implementation
sed -n '71,95p' src/lib/github/client.tsRepository: AOSSIE-Org/OrgExplorer
Length of output: 695
Extract rate limit info from response headers instead of preflight checks.
Calling getRateLimit() before every request attempt adds a separate API call, doubling traffic and consuming unnecessary quota. GitHub API responses include x-ratelimit-remaining and x-ratelimit-reset headers that should be extracted from the actual request and used for backoff decisions, eliminating the preflight check entirely.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/github/client.ts` around lines 133 - 140, The current loop calls
getRateLimit() before each request which doubles API calls; instead remove the
preflight getRateLimit() invocation in the retry loop and extract rate-limit
info from the actual request response headers (e.g., read
response.headers.get('x-ratelimit-remaining') and 'x-ratelimit-reset') to decide
backoff and delay; update the retry logic inside the for-loop that uses
maxRetries/attempt and delay() so it inspects those headers after a response (or
on error when available) and computes waitTime = (reset - Date.now()/1000)*1000,
logging and awaiting delay(waitTime) when remaining < 1 and waitTime > 0.
| const perPage = 100; | ||
|
|
||
| while (allIssues.length < maxResults) { | ||
| const query = `org:${org} is:issue is:open${sinceDate ? ` created:>${sinceDate}` : ''}`; |
There was a problem hiding this comment.
searchAllIssues currently fetches only open issues.
Line 285 hardcodes is:open, so closed issues are excluded. That undercounts contributor activity for an “all issues” API.
💡 Proposed fix
- const query = `org:${org} is:issue is:open${sinceDate ? ` created:>${sinceDate}` : ''}`;
+ const query = `org:${org} is:issue${sinceDate ? ` created:>${sinceDate}` : ''}`;📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const query = `org:${org} is:issue is:open${sinceDate ? ` created:>${sinceDate}` : ''}`; | |
| const query = `org:${org} is:issue${sinceDate ? ` created:>${sinceDate}` : ''}`; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/github/client.ts` at line 285, The searchAllIssues implementation
currently forces only open issues by building the query string with "is:open";
update the query construction in searchAllIssues to remove or make the "is:open"
filter conditional so it can include closed issues when callers request "all"
(or default to no state filter for an all-issues endpoint). Locate the query
string assembly where `const query = \`org:${org} is:issue is:open${sinceDate ?
\` created:>${sinceDate}\` : ''}\`;` is defined and adjust it to either omit
`is:open` or accept a parameter (e.g., issueState) to append `is:open` or
`is:closed` only when needed, ensuring callers can request all issues. Ensure
tests or call sites of searchAllIssues are updated to pass the desired state
when appropriate.
| export interface ContributorSummary { | ||
| login: string; | ||
| avatarUrl: string; | ||
| pullRequestCount: number; | ||
| issueCount: number; | ||
| mergedPRCount: number; | ||
| repositories: Map<string, number>; | ||
| totalContributions: number; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check how ContributorSummary is constructed and if Map serialization is handled
rg -n "ContributorSummary|repositories.*Map|new Map" --type ts -A 3 -B 1Repository: AOSSIE-Org/OrgExplorer
Length of output: 4007
🏁 Script executed:
#!/bin/bash
# Find where fetchOrgActivity is used and how the result is handled
rg -n "fetchOrgActivity|ContributorMatrixData" --type ts --type tsx -A 5 -B 2 | head -80Repository: AOSSIE-Org/OrgExplorer
Length of output: 93
🏁 Script executed:
#!/bin/bash
# Find where fetchOrgActivity is used and how the result is handled
rg -n "fetchOrgActivity|ContributorMatrixData" --type ts -A 5 -B 2 | head -100Repository: AOSSIE-Org/OrgExplorer
Length of output: 6954
🏁 Script executed:
#!/bin/bash
# Get the full ExportContributorDataUseCase implementation
sed -n '80,95p' src/features/contributor-activity/useCases.tsRepository: AOSSIE-Org/OrgExplorer
Length of output: 677
Fix JSON serialization of Map fields in export function.
The ExportContributorDataUseCase.execute method calls JSON.stringify(data) directly on ContributorMatrixData, which contains Map<string, number> fields (repositories and activityByContributor). Maps serialize as empty objects {}, causing complete data loss in JSON exports. The CSV export correctly converts Maps using Array.from(entries()), but the JSON export does not. Convert Maps to plain objects before serialization or implement a custom replacer function for JSON.stringify.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/github/types.ts` around lines 34 - 42, The JSON export currently
serializes ContributorMatrixData containing Map fields (repositories and
activityByContributor) which become empty objects; in
ExportContributorDataUseCase.execute convert any Map<string, number> fields
(e.g., ContributorSummary.repositories and top-level activityByContributor in
ContributorMatrixData) into plain objects before calling JSON.stringify — for
example, replace Maps with Object.fromEntries(map) or build a plain object via
Array.from(map.entries()) for each map, or supply a JSON.stringify replacer that
transforms Map instances to plain objects; ensure you perform this
transformation for ContributorSummary objects inside the matrix as well as the
top-level maps so the exported JSON contains the actual key/value pairs.
| export interface RateLimitInfo { | ||
| limit: number; | ||
| remaining: number; | ||
| reset: number; | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major
Duplicate RateLimitInfo interface definition.
This interface is already defined identically in src/lib/github/client.ts (lines 2-6). Consider removing one and importing from a single source to avoid maintenance burden and potential drift.
♻️ Suggested approach
Either:
- Export from
types.tsand import inclient.ts, or - Remove from
types.tsand import fromclient.ts
-export interface RateLimitInfo {
- limit: number;
- remaining: number;
- reset: number;
-}
+// Re-export from client.ts or consolidate all types here
+export type { RateLimitInfo } from './client';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/github/types.ts` around lines 56 - 60, The RateLimitInfo interface is
duplicated; consolidate to a single source by keeping one declaration and
importing it where needed: either export RateLimitInfo from the types module and
remove the duplicate in the client module (then import RateLimitInfo in
client.ts), or delete RateLimitInfo from types.ts and import the single
definition from client.ts; update any imports/usages to reference the retained
symbol RateLimitInfo and remove the redundant interface declaration.
fix #30
Added Contributor Activity Matrix feature that tracks PRs, issues, and repositories across GitHub organizations with caching, rate limit handling, and optional token for higher limits.
demo video share in discord .
Summary by CodeRabbit
Release Notes
New Features
Chores